From 8d5c9957d12c1bd9c1ff618cc583dfb40c3e8f5d Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Wed, 6 Dec 2023 22:15:09 -0600 Subject: [PATCH 1/6] added 10 min timeout --- agent.js | 6 ++++-- utils/coder.js | 34 +++++++++++++++++++++++++++------- utils/skills.js | 4 ++++ 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/agent.js b/agent.js index e6491c3..46f7937 100644 --- a/agent.js +++ b/agent.js @@ -69,8 +69,10 @@ export class Agent { this.coder.queueCode(code); let code_return = await this.coder.execute(); let message = code_return.message; - if (code_return.interrupted) - break; // can only be interrupted by another chat, so this chat is over. + if (code_return.timedout) + message += "\n Code ran for a while and was stopped."; + else if (code_return.interrupted) + break; // when interupted but not timed out, we were interupted by another conversation. end this one. if (!code_return.success) { message += "\n Write code to fix the problem and try again."; } diff --git a/utils/coder.js b/utils/coder.js index b80c026..e13e417 100644 --- a/utils/coder.js +++ b/utils/coder.js @@ -11,6 +11,7 @@ export class Coder { this.executing = false; this.agent.bot.output = ''; this.code_template = ''; + this.timedout = false; readFile(this.fp+'template.js', 'utf8', (err, data) => { if (err) throw err; @@ -49,8 +50,8 @@ export class Coder { // returns {success: bool, message: string, interrupted: bool} async execute() { - if (!this.queued_code) return {success: false, message: "No code to execute.", interrupted: false}; - if (!this.code_template) return {success: false, message: "Code template not loaded.", interrupted: false}; + if (!this.queued_code) return {success: false, message: "No code to execute.", interrupted: false, timedout: false}; + if (!this.code_template) return {success: false, message: "Code template not loaded.", interrupted: false, timedout: false}; let src = ''; let code = this.queued_code; @@ -80,7 +81,7 @@ export class Coder { if (write_result) { console.error('Error writing code execution file: ' + result); - return {success: false, message: result, interrupted: false}; + return {success: false, message: result, interrupted: false, timedout: false}; } try { @@ -90,14 +91,16 @@ export class Coder { this.current_code = this.queued_code; this.executing = true; - await execution_file.main(this.agent.bot); + const TIMEOUT = this._startTimeout(10); + await execution_file.main(this.agent.bot); // open fire + clearTimeout(TIMEOUT); this.executing = false; - this.agent.bot.emit('finished_executing'); let output = this.formatOutput(this.agent.bot); let interrupted = this.agent.bot.interrupt_code; + let timedout = this.timedout; this.clear(); - return {success:true, message: output, interrupted}; + return {success:true, message: output, interrupted, timedout}; } catch (err) { this.executing = false; this.agent.bot.emit('finished_executing'); @@ -106,7 +109,7 @@ export class Coder { message += '!!Code threw exception!! Error: ' + err; let interrupted = this.agent.bot.interrupt_code; await this.stop(); - return {success: false, message, interrupted}; + return {success: false, message, interrupted, timedout: false}; } } @@ -125,6 +128,7 @@ export class Coder { } async stop() { + if (!this.executing) return; while (this.executing) { this.agent.bot.interrupt_code = true; this.agent.bot.collectBlock.cancelTask(); @@ -140,5 +144,21 @@ export class Coder { this.current_code = ''; this.agent.bot.output = ''; this.agent.bot.interrupt_code = false; + this.timedout = false; + } + + _startTimeout(TIMEOUT_MINS=10) { + return setTimeout(async () => { + console.warn(`Code execution timed out after ${TIMEOUT_MINS} minutes. Attempting force stop.`); + this.timedout = true; + this.stop(); // last attempt to stop + await new Promise(resolve => setTimeout(resolve, 5 * 1000)); + if (this.executing) { + console.error(`Failed to stop. Killing process. Goodbye.`); + // TODO: force save memories + process.exit(1); // force exit program + } + console.log('Code execution stopped successfully.'); + }, TIMEOUT_MINS*60*1000); } } \ No newline at end of file diff --git a/utils/skills.js b/utils/skills.js index c024b0d..8b10953 100644 --- a/utils/skills.js +++ b/utils/skills.js @@ -227,6 +227,10 @@ export async function collectBlock(bot, blockType, num=1) { * @example * await skills.collectBlock(bot, "oak_log"); **/ + if (num < 1) { + log(bot, `Invalid number of blocks to collect: ${num}.`); + return false; + } let collected = 0; const blocks = getNearestBlocks(bot, blockType, 64, num); if (blocks.length === 0) { From e2aad3df9c830a3eb3d146cf01476006bd887d61 Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Fri, 8 Dec 2023 16:18:20 -0600 Subject: [PATCH 2/6] added parent process + crash restarts --- agent.js | 6 ++---- controller.js | 32 ++++++++++++++++++++++++++++++++ init_agent.js | 22 ++++++++++++++++++++++ main.js | 4 ---- package.json | 3 ++- utils/coder.js | 12 ++++++++---- utils/skills.js | 5 +++-- 7 files changed, 69 insertions(+), 15 deletions(-) create mode 100644 controller.js create mode 100644 init_agent.js delete mode 100644 main.js diff --git a/agent.js b/agent.js index 46f7937..f1c6bdb 100644 --- a/agent.js +++ b/agent.js @@ -69,12 +69,10 @@ export class Agent { this.coder.queueCode(code); let code_return = await this.coder.execute(); let message = code_return.message; - if (code_return.timedout) - message += "\n Code ran for a while and was stopped."; - else if (code_return.interrupted) + if (code_return.interrupted && !code_return.timedout) break; // when interupted but not timed out, we were interupted by another conversation. end this one. if (!code_return.success) { - message += "\n Write code to fix the problem and try again."; + message += "\nWrite code to fix the problem and try again."; } console.log('code return:', message); this.history.add('system', message); diff --git a/controller.js b/controller.js new file mode 100644 index 0000000..9db3a83 --- /dev/null +++ b/controller.js @@ -0,0 +1,32 @@ +import { spawn } from 'child_process'; + +class AgentController { + constructor(name) { + this.name = name; + } + async start(restart_memory=false) { + let args = ['init_agent.js', this.name]; + if (restart_memory) + args.push('-r'); + const agentProcess = spawn('node', args, { + stdio: 'inherit', + stderr: 'inherit', + }); + + agentProcess.on('exit', (code, signal) => { + console.log(`Agent process exited with code ${code} and signal ${signal}`); + + // Restart the agent if it exited due to an error + if (code !== 0) { + console.log('Restarting agent...'); + this.start(); + } + }); + + agentProcess.on('error', (err) => { + console.error('Failed to start agent process:', err); + }); + } +} + +new AgentController('andy').start(); \ No newline at end of file diff --git a/init_agent.js b/init_agent.js new file mode 100644 index 0000000..43a0615 --- /dev/null +++ b/init_agent.js @@ -0,0 +1,22 @@ +import { Agent } from './agent.js'; +import yargs from 'yargs'; + +const args = process.argv.slice(2); +if (args.length < 1) { + console.log('Usage: node init_agent.js [options]'); + process.exit(1); +} + +const argv = yargs(args) + .option('restart_memory', { + alias: 'r', + type: 'boolean', + description: 'restart memory from scratch' + }).argv; + +const name = argv._[0]; +const restart_memory = !!argv.restart_memory; +const save_path = './bots/'+name+'.json'; + +let agent = new Agent(name, save_path, restart_memory); +agent.start(); diff --git a/main.js b/main.js deleted file mode 100644 index 7d56f81..0000000 --- a/main.js +++ /dev/null @@ -1,4 +0,0 @@ -import { Agent } from './agent.js'; - -let agent = new Agent('andy', './bots/andy.json', true); -agent.start(); diff --git a/package.json b/package.json index 401e918..39ea3ce 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "mineflayer-pathfinder": "^2.4.4", "mineflayer-pvp": "^1.3.2", "openai": "^4.4.0", - "patch-package": "^8.0.0" + "patch-package": "^8.0.0", + "yargs": "^17.7.2" } } diff --git a/utils/coder.js b/utils/coder.js index e13e417..5bc029c 100644 --- a/utils/coder.js +++ b/utils/coder.js @@ -48,7 +48,7 @@ export class Coder { } - // returns {success: bool, message: string, interrupted: bool} + // returns {success: bool, message: string, interrupted: bool, timedout: false} async execute() { if (!this.queued_code) return {success: false, message: "No code to execute.", interrupted: false, timedout: false}; if (!this.code_template) return {success: false, message: "Code template not loaded.", interrupted: false, timedout: false}; @@ -114,7 +114,7 @@ export class Coder { } formatOutput(bot) { - if (bot.interrupt_code) return ''; + if (bot.interrupt_code && !this.timedout) return ''; let output = bot.output; const MAX_OUT = 1000; if (output.length > MAX_OUT) { @@ -151,11 +151,15 @@ export class Coder { return setTimeout(async () => { console.warn(`Code execution timed out after ${TIMEOUT_MINS} minutes. Attempting force stop.`); this.timedout = true; + this.agent.bot.output += `\nAction performed for ${TIMEOUT_MINS} minutes and then timed out and stopped. You may want to continue or do something else.`; this.stop(); // last attempt to stop - await new Promise(resolve => setTimeout(resolve, 5 * 1000)); + await new Promise(resolve => setTimeout(resolve, 5 * 1000)); // wait 5 seconds if (this.executing) { console.error(`Failed to stop. Killing process. Goodbye.`); - // TODO: force save memories + this.agent.bot.output += `\nForce stop failed! Killing bot.`; + let output = this.formatOutput(this.agent.bot); + this.agent.history.add('system', output); + this.agent.history.save(); process.exit(1); // force exit program } console.log('Code execution stopped successfully.'); diff --git a/utils/skills.js b/utils/skills.js index 8b10953..9d97a0d 100644 --- a/utils/skills.js +++ b/utils/skills.js @@ -259,7 +259,9 @@ export async function collectBlock(bot, blockType, num=1) { log(bot, `Failed to collect ${blockType}: ${err}.`); continue; } - } + } + if (bot.interrupt_code) + break; } log(bot, `Collected ${collected} ${blockType}.`); return true; @@ -569,7 +571,6 @@ export async function followPlayer(bot, username) { log(bot, `You are now actively following player ${username}.`); while (!bot.interrupt_code) { - console.log('followPlayer waiting for interrupt...', bot.interrupt_code); await new Promise(resolve => setTimeout(resolve, 1000)); } From 597e7d02b8db4daca70a12d8d7fe45acb5cbc12d Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Sat, 9 Dec 2023 21:40:53 -0600 Subject: [PATCH 3/6] refactor/restart improvements --- agent.js | 5 +++- .../agent-controller.js | 23 +++++++++++-------- init_agent.js => controller/init-agent.js | 2 +- main.js | 3 +++ utils/coder.js | 4 +++- utils/history.js | 2 +- utils/{skill_library.js => skill-library.js} | 0 7 files changed, 26 insertions(+), 13 deletions(-) rename controller.js => controller/agent-controller.js (54%) rename init_agent.js => controller/init-agent.js (93%) create mode 100644 main.js rename utils/{skill_library.js => skill-library.js} (100%) diff --git a/agent.js b/agent.js index f1c6bdb..0f9ae95 100644 --- a/agent.js +++ b/agent.js @@ -3,7 +3,7 @@ import { sendRequest } from './utils/gpt.js'; import { History } from './utils/history.js'; import { Coder } from './utils/coder.js'; import { getQuery, containsQuery } from './utils/queries.js'; -import { containsCodeBlock } from './utils/skill_library.js'; +import { containsCodeBlock } from './utils/skill-library.js'; export class Agent { @@ -20,6 +20,9 @@ export class Agent { this.bot.on('login', () => { this.bot.chat('Hello world! I am ' + this.name); console.log(`${this.name} logged in.`); + if (!restart_memory) { + this.respond('system', 'Agent process restarted. Notify the user and decide what to do.'); + } }); } diff --git a/controller.js b/controller/agent-controller.js similarity index 54% rename from controller.js rename to controller/agent-controller.js index 9db3a83..382421a 100644 --- a/controller.js +++ b/controller/agent-controller.js @@ -1,25 +1,32 @@ import { spawn } from 'child_process'; -class AgentController { +export class AgentController { constructor(name) { this.name = name; } - async start(restart_memory=false) { - let args = ['init_agent.js', this.name]; + async start(restart_memory=true) { + let args = ['controller/init-agent.js', this.name]; if (restart_memory) args.push('-r'); + const agentProcess = spawn('node', args, { stdio: 'inherit', stderr: 'inherit', }); - + + let last_restart = Date.now(); agentProcess.on('exit', (code, signal) => { console.log(`Agent process exited with code ${code} and signal ${signal}`); - // Restart the agent if it exited due to an error if (code !== 0) { + // agent must run for at least 30 seconds before restarting + if (Date.now() - last_restart < 30 * 1000) { + console.error('Agent process exited too quickly. Killing entire process. Goodbye.'); + process.exit(1); + } console.log('Restarting agent...'); - this.start(); + this.start(false); + last_restart = Date.now(); } }); @@ -27,6 +34,4 @@ class AgentController { console.error('Failed to start agent process:', err); }); } -} - -new AgentController('andy').start(); \ No newline at end of file +} \ No newline at end of file diff --git a/init_agent.js b/controller/init-agent.js similarity index 93% rename from init_agent.js rename to controller/init-agent.js index 43a0615..465ceb2 100644 --- a/init_agent.js +++ b/controller/init-agent.js @@ -1,4 +1,4 @@ -import { Agent } from './agent.js'; +import { Agent } from '../agent.js'; import yargs from 'yargs'; const args = process.argv.slice(2); diff --git a/main.js b/main.js new file mode 100644 index 0000000..d601ded --- /dev/null +++ b/main.js @@ -0,0 +1,3 @@ +import { AgentController } from './controller/agent-controller.js'; + +new AgentController('andy').start(); \ No newline at end of file diff --git a/utils/coder.js b/utils/coder.js index 5bc029c..5ea4341 100644 --- a/utils/coder.js +++ b/utils/coder.js @@ -95,6 +95,7 @@ export class Coder { await execution_file.main(this.agent.bot); // open fire clearTimeout(TIMEOUT); this.executing = false; + this.agent.bot.emit('finished_executing'); let output = this.formatOutput(this.agent.bot); let interrupted = this.agent.bot.interrupt_code; @@ -156,7 +157,8 @@ export class Coder { await new Promise(resolve => setTimeout(resolve, 5 * 1000)); // wait 5 seconds if (this.executing) { console.error(`Failed to stop. Killing process. Goodbye.`); - this.agent.bot.output += `\nForce stop failed! Killing bot.`; + this.agent.bot.output += `\nForce stop failed! Process was killed and will be restarted. Goodbye world.`; + this.bot.chat('Goodbye world.'); let output = this.formatOutput(this.agent.bot); this.agent.history.add('system', output); this.agent.history.save(); diff --git a/utils/history.js b/utils/history.js index b14a909..abaa323 100644 --- a/utils/history.js +++ b/utils/history.js @@ -1,6 +1,6 @@ import { writeFileSync, readFileSync, mkdirSync } from 'fs'; import { getQueryDocs } from './queries.js'; -import { getSkillDocs } from './skill_library.js'; +import { getSkillDocs } from './skill-library.js'; import { sendRequest, embed, cosineSimilarity } from './gpt.js'; diff --git a/utils/skill_library.js b/utils/skill-library.js similarity index 100% rename from utils/skill_library.js rename to utils/skill-library.js From 663f7f9b1ae85e071b8e90723d75cb5c77fbe1e9 Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Sun, 10 Dec 2023 20:18:20 -0600 Subject: [PATCH 4/6] refactored init code --- agent.js | 48 +++++++++---------- .../{agent-controller.js => agent-process.js} | 17 ++++--- controller/init-agent.js | 19 +++++--- main.js | 4 +- 4 files changed, 45 insertions(+), 43 deletions(-) rename controller/{agent-controller.js => agent-process.js} (76%) diff --git a/agent.js b/agent.js index 0f9ae95..7263ad6 100644 --- a/agent.js +++ b/agent.js @@ -7,45 +7,43 @@ import { containsCodeBlock } from './utils/skill-library.js'; export class Agent { - constructor(name, save_path, restart_memory=false) { + constructor(name, save_path, clear_memory=false, autostart=false) { this.name = name; this.bot = initBot(name); this.history = new History(this, save_path); + this.history.loadExamples(); this.coder = new Coder(this); - if (!restart_memory) { + if (!clear_memory) { this.history.load(); } - + this.bot.on('login', () => { this.bot.chat('Hello world! I am ' + this.name); console.log(`${this.name} logged in.`); - if (!restart_memory) { + + this.bot.on('chat', (username, message) => { + if (username === this.name) return; + console.log('received message from', username, ':', message); + + this.respond(username, message); + this.history.save(); + }); + + this.bot.on('finished_executing', () => { + setTimeout(() => { + if (!this.coder.executing) { + // return to default behavior + } + }, 10000); + }); + + if (autostart) this.respond('system', 'Agent process restarted. Notify the user and decide what to do.'); - } + }); } - async start() { - await this.history.loadExamples(); - - this.bot.on('chat', (username, message) => { - if (username === this.name) return; - console.log('received message from', username, ':', message); - - this.respond(username, message); - this.history.save(); - }); - - this.bot.on('finished_executing', () => { - setTimeout(() => { - if (!this.coder.executing) { - // return to default behavior - } - }, 10000); - }) - } - async respond(username, message) { await this.history.add(username, message); for (let i=0; i<5; i++) { diff --git a/controller/agent-controller.js b/controller/agent-process.js similarity index 76% rename from controller/agent-controller.js rename to controller/agent-process.js index 382421a..cfeb159 100644 --- a/controller/agent-controller.js +++ b/controller/agent-process.js @@ -1,13 +1,12 @@ import { spawn } from 'child_process'; -export class AgentController { - constructor(name) { - this.name = name; - } - async start(restart_memory=true) { - let args = ['controller/init-agent.js', this.name]; - if (restart_memory) - args.push('-r'); +export class AgentProcess { + constructor(name, clear_memory=false, autostart=false) { + let args = ['controller/init-agent.js', name]; + if (clear_memory) + args.push('-c'); + if (autostart) + args.push('-a'); const agentProcess = spawn('node', args, { stdio: 'inherit', @@ -25,7 +24,7 @@ export class AgentController { process.exit(1); } console.log('Restarting agent...'); - this.start(false); + this.start(false, true); last_restart = Date.now(); } }); diff --git a/controller/init-agent.js b/controller/init-agent.js index 465ceb2..e632064 100644 --- a/controller/init-agent.js +++ b/controller/init-agent.js @@ -3,20 +3,25 @@ import yargs from 'yargs'; const args = process.argv.slice(2); if (args.length < 1) { - console.log('Usage: node init_agent.js [options]'); + console.log('Usage: node init_agent.js [-c] [-a]'); process.exit(1); } const argv = yargs(args) - .option('restart_memory', { - alias: 'r', + .option('clear_memory', { + alias: 'c', type: 'boolean', description: 'restart memory from scratch' - }).argv; + }) + .option('autostart', { + alias: 'a', + type: 'boolean', + description: 'automatically prompt the agent on startup' + }).argv const name = argv._[0]; -const restart_memory = !!argv.restart_memory; +const clear_memory = !!argv.clear_memory; +const autostart = !!argv.autostart; const save_path = './bots/'+name+'.json'; -let agent = new Agent(name, save_path, restart_memory); -agent.start(); +new Agent(name, save_path, clear_memory, autostart); diff --git a/main.js b/main.js index d601ded..9ed8af8 100644 --- a/main.js +++ b/main.js @@ -1,3 +1,3 @@ -import { AgentController } from './controller/agent-controller.js'; +import { AgentProcess } from './controller/agent-process.js'; -new AgentController('andy').start(); \ No newline at end of file +new AgentProcess('andy', true, false); \ No newline at end of file From 48fd28a10f45ad5294fca6c29177898221f9b8cd Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Sun, 10 Dec 2023 20:19:07 -0600 Subject: [PATCH 5/6] fixed timeout+smeltitem --- utils/coder.js | 10 ++++++---- utils/skills.js | 3 +-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/utils/coder.js b/utils/coder.js index 5ea4341..61f3211 100644 --- a/utils/coder.js +++ b/utils/coder.js @@ -83,7 +83,7 @@ export class Coder { console.error('Error writing code execution file: ' + result); return {success: false, message: result, interrupted: false, timedout: false}; } - + let TIMEOUT; try { console.log('executing code...\n'); let execution_file = await import('.'+filename); @@ -91,10 +91,10 @@ export class Coder { this.current_code = this.queued_code; this.executing = true; - const TIMEOUT = this._startTimeout(10); + TIMEOUT = this._startTimeout(1); await execution_file.main(this.agent.bot); // open fire - clearTimeout(TIMEOUT); this.executing = false; + clearTimeout(TIMEOUT); this.agent.bot.emit('finished_executing'); let output = this.formatOutput(this.agent.bot); @@ -104,8 +104,10 @@ export class Coder { return {success:true, message: output, interrupted, timedout}; } catch (err) { this.executing = false; + clearTimeout(TIMEOUT); + this.agent.bot.emit('finished_executing'); - console.error("Code execution triggered catch:" + err); + console.error("Code execution triggered catch: " + err); let message = this.formatOutput(this.agent.bot); message += '!!Code threw exception!! Error: ' + err; let interrupted = this.agent.bot.interrupt_code; diff --git a/utils/skills.js b/utils/skills.js index 9d97a0d..2b119bc 100644 --- a/utils/skills.js +++ b/utils/skills.js @@ -57,13 +57,12 @@ export async function smeltItem(bot, itemName, num=1) { let furnaceBlock = undefined; furnaceBlock = getNearestBlock(bot, 'furnace', 6); - if (furnaceBlock === null){ + if (!furnaceBlock){ log(bot, `There is no furnace nearby.`) return false; } await bot.lookAt(furnaceBlock.position); - console.log('smelting...'); const furnace = await bot.openFurnace(furnaceBlock); // check if the furnace is already smelting something From 90ac954c0459bb187a06da1aa2f5eea79e69badf Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Tue, 12 Dec 2023 13:39:35 -0600 Subject: [PATCH 6/6] fixed restart --- controller/agent-process.js | 7 +++++-- main.js | 2 +- utils/coder.js | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/controller/agent-process.js b/controller/agent-process.js index cfeb159..53fdf1b 100644 --- a/controller/agent-process.js +++ b/controller/agent-process.js @@ -1,8 +1,11 @@ import { spawn } from 'child_process'; export class AgentProcess { - constructor(name, clear_memory=false, autostart=false) { - let args = ['controller/init-agent.js', name]; + constructor(name) { + this.name = name; + } + start(clear_memory=false, autostart=false) { + let args = ['controller/init-agent.js', this.name]; if (clear_memory) args.push('-c'); if (autostart) diff --git a/main.js b/main.js index 9ed8af8..ea78372 100644 --- a/main.js +++ b/main.js @@ -1,3 +1,3 @@ import { AgentProcess } from './controller/agent-process.js'; -new AgentProcess('andy', true, false); \ No newline at end of file +new AgentProcess('andy').start(true, false); \ No newline at end of file diff --git a/utils/coder.js b/utils/coder.js index 61f3211..f6040d1 100644 --- a/utils/coder.js +++ b/utils/coder.js @@ -91,7 +91,7 @@ export class Coder { this.current_code = this.queued_code; this.executing = true; - TIMEOUT = this._startTimeout(1); + TIMEOUT = this._startTimeout(10); await execution_file.main(this.agent.bot); // open fire this.executing = false; clearTimeout(TIMEOUT); @@ -160,7 +160,7 @@ export class Coder { if (this.executing) { console.error(`Failed to stop. Killing process. Goodbye.`); this.agent.bot.output += `\nForce stop failed! Process was killed and will be restarted. Goodbye world.`; - this.bot.chat('Goodbye world.'); + this.agent.bot.chat('Goodbye world.'); let output = this.formatOutput(this.agent.bot); this.agent.history.add('system', output); this.agent.history.save();